gltfクレートによる3Dモデルの読み込み(とバグ取り) < rusterizer-from-dots
https://gyazo.com/1eade565fe9711c8a798502f244ea151
ようやく光の表現とか影の表現がやりやすい環境が整いつつある!
Phongシェーダーに行きたい
でも、その前に今の造形でそれをやろうとするのはめちゃ大変なのでちょっと考えたい。
できれば、外部ファイルからモデルを読み込みたいappbird.icon
fbxってよく聞くし、fbxを読むことを考えようかな
目次
Rustとfbx
---> fbx読むの大変そう
gltfによる3Dモデルの読み込み
そもそもfbxって自力でパーサーかける?どういう仕様なんすか?appbird.icon
簡単に読めるなら組んでみようかな
FBX はもともと Kaydara が開発した “Filmbox” というモーションキャプチャ/3Dデータ管理ソフトのフォーマットを起源とします。その後 Autodesk が買収・発展させました。GPT.icon
フォーマット自体はプロプライエタリ(非公開仕様)で、Autodesk が提供する FBX SDK を使って読み書きするのが正式な手段とされています。
!!?!?!?!?!appbird.icon じゃあBlenderで触れるのは一体....
テキスト形式とバイナリ形式があるよGPT.icon
code:txt
NodeType: プロパティリスト {
子ノード1 : プロパティ… { … }
子ノード2 : プロパティ… { … }
…
}
はぇ~appbird.icon 形式自体は簡単っぽそうだけど、いろんなデータの種類に対応していくのはかなり面倒くさそうだ
バイナリ形式はより複雑ですが、リバースエンジニアリングによって仕様がかなり判明しています。GPT.icon
With no public documentation available on the binary FBX format, Blender Foundation asked Alexander to write up what he has figured out sofar. We hope this will lead to better interoperability of 3D applications in general. Blender”s next release (2.69) will support binary FBX file reading as well.
....3D界隈も結構大変な基盤の作り方されてんねぇ.....appbird.icon
う~~ん、こりゃ複雑かもな
仕様書を読み解いてやるのも大事だろうけど、今回はスキップ また今度やるね
...うーん大変そうだなぁappbird.icon
というわけでライブラリに頼りたい
fbxcel使うといいでGPT.icon バイト列読むためのツールと高レベルに読むツールの二種類があるで
FBXの構造を強く型付けしてるでGPT.icon
はぇ~appbird.icon
こんな感じに頂点データとかを抽出できるGPT.icon
このコードは動かなかった....(多分バージョン違いかなぁ)appbird.icon
code:rs
use fbxcel_dom::FbxDom; // ---> ?
use anyhow::Result;
use std::fs::File;
fn main() -> Result<()> {
let file = File::open("model.fbx")?;
let dom = FbxDom::from_reader(file)?;
// Geometryノードを探索
for geom in dom.objects().iter().filter(|o| o.class() == "Geometry") {
if let Some(data) = geom.properties().get("Vertices") {
println!("Vertices: {:?}", data);
}
if let Some(data) = geom.properties().get("PolygonVertexIndex") {
println!("Indices: {:?}", data);
}
}
Ok(())
}
読み込んだfbxをvaoに収めよう
fbx
code:cmd
cargo add fbxcel fbxcel-dom
まず、fbxのファイルをロードしようappbird.icon
code:rs
fn load_vao(path:&Path) -> Result<VertexArrayObject<texture_pipeline::Attribute>> {
let file = File::open(path)?;
let reader = std::io::BufReader::new(file);
match AnyDocument::from_seekable_reader(reader).expect("Failed to load document") {
AnyDocument::V7400(fbx_ver, doc) => {
todo!()
}
_ => panic!("Got FBX document of unsupported version.")
};
Ok(())
}
欲しいのは頂点データとColorのデータかなappbird.icon
...どうやってやればいいんだ?
なんかドキュメントも整備されてないし...うん!?未完成!?
If you want to interpret and render FBX data, you need another library or need to do it yourself. (fbxcel-dom is incomplete and currently unmaintained, but it can help you know what kind of tasks are needed to interpret FBX data.)
...自力でやるのってどれぐらい難しい?appbird.icon
めっちゃめんどいでGPT.icon 頂点データの読み取りとかアニメーションデータの読み取りとか、仕様を読み解きながら数百行の実装が必要
ほなやめとくか....appbird.icon
objはどう?GPT.icon
いやアニメーションないしな...appbird.icon
2025-10-14
glTF: The JPEG of 3D
The core of glTF is a JSON file that describes the structure and composition of a scene containing 3D models, which can be stored in a single binary glTF file (.glb). The top-level elements of the file include: Scenes and nodes, cameras, meshes, buffers, materials, textures, skins and animations.
GPT.iconいわくfbxを置き換える勢いとか言われてるけど本当?appbird.icon
あぁでも確かにデータ形式もすごく読みやすいなこれappbird.icon
よし、これにするかappbird.icon
というわけで、まずWorldのフォルダを整えていくかねappbird.icon
今Worldフォルダにいろいろありすぎて。。。
code:powershell
PS ...> tree /f ./renderer_core/src/world
actor.rs
camera.rs
mesh.rs
mesh_renderer.rs
plane.rs
tetrahedron.rs
textured_mesh_renderer.rs
transform.rs
world.rs
リファクタリング
/componentと/actorにわけていくよ
/meshにはmesh.rs(今のVertexArrayObjectが入ってるやつ)を入れるよ
名前はvaoにする
code:mesh.rs
mod vao;
pub use vao::VertexArrayObject;
動作確認◎
まずgltfの基本的な使い方とテスト
AC.icon
今回はMeshのデータを読みたいので、Meshのドキュメントを漁ってExampleを見てみよう。
こうかな いったんテストしてみよう。
code:mesh/mesh_loader.rs
use std::path::Path;
use gltf::Gltf;
use crate::{mesh::VertexArrayObject, shader::texture_pipeline, util::Throwable};
fn load_vao(path:&Path)
-> Throwable<()> {
//-> Throwable<VertexArrayObject<texture_pipeline::Attribute>> {
let model = Gltf::open(path)?;
for mesh in model.meshes() {
println!("Mesh #{}", mesh.index()); for primitive in mesh.primitives() {
println!("- primitive #{}", primitive.index()); for (semantic, _) in primitive.attributes() {
println!("-- {semantic:?}");
}
}
}
Ok(())
}
まずBlenderでトーラスでも作りますappbird.icon
カラー属性つけたいな
カラー属性どうやっていじるんや
https://youtu.be/hqBJtM8y064?si=Uud5Vk8dxWFz1OtY
https://gyazo.com/d162cbb503089d4f2f00a5b040d29164
いかにもマリルイで出てきそうなオブジェが出来てしまった...appbird.icon
まず、既存のtetrahedronのデモと、mesh_loadのデモをsrc/binに写しておく
まっさらになったmainのコードにglTFのロードのコードを組みこんでいく!
期待するのは、ファイルの中身の属性値を全部出力することだけどいけるかなappbird.icon
code:main.rs
use std::path::Path;
use renderer_core::mesh::mesh_loader::load_vao;
use renderer_core::world::camera::Camera;
use renderer_core::canvas::Canvas;
use renderer_core::util::Throwable;
fn main() -> Throwable<()> {
let w: usize = 640;
let h: usize = 480;
let aspect = (h as f64) / (w as f64);
let mut _canvas = Canvas::new(w, h)?;
let mut _camera = Camera::new(aspect);
load_vao(&Path::new("./resource/test_model.glb"))?;
Ok(())
}
code:stdout
-- Positions
-- Normals
-- Colors(0)
-- TexCoords(0)
おおお!!!appbird.icon
いけてるわねappbird.icon
しかし、Colorsの後の(0)はなんだ...?appbird.icon
あぁ、0番目の色指定ってことか
まぁともかく、Colorsから始まるものを取り出せばいいのかな
いや、そんな安全じゃないことやる必要あるか
Exampleを続けて読むと、readerがあった!
code:rs
let (model, buffers, _) = gltf::import(path)?;
:
if let Some(iter) = reader.read_positions() {
println!("vertex position");
for vertex_position in iter.take(5) {
println!("{vertex_position:?}");
}
}
なるほど、これで頂点情報、カラー情報、インデックスバッファ情報全部取ってみるかappbird.icon
code:stdout
vertex position
vertex color
vertex index
1
50
55
1
55
7
5
53
59
5
見た感じ、vertex indexは本当に三角形ポリゴンの頂点添字列が3つ並びで1次元配列に沿って並んでるっぽいなappbird.icon
ほなそんな感じにロードしてみるかappbird.icon
gltfのデータをVertexArrayObjectに変換するappbird.icon code:rs
let positions =
reader.read_positions().expect("The model data doesn't have position data.")
.map(|point| Vec4::newpoint(point0 as f64, point1 as f64, point2 as f64)) .collect::<Vec<_>>();
let colors =
reader.read_colors(0).expect("The model data doesn't have color data.")
.into_rgb_f32()
.map(|color| Vec4::newvec(color0 as f64, color1 as f64, color2 as f64)) .collect::<Vec<_>>();
ん...?Vec<usize>をVec<[usize; 3]>にするのどうすりゃいいんだ...?appbird.icon
単純なforで書く場合にはindexで3ずつ送りにするほかはないのかな
そういうメソッドはないのだろうかappbird.icon
chunks_exactがあるでGPT.icon
なるほど
2025-10-15
code:rs
let idcs =
reader.read_indices()
.expect("The model data doesn't have indices data.")
.into_u32()
.collect::<Vec<_>>()
.chunks_exact(3)
.map(|idx| [idx0, idx1, idx2]); あとはprimitiveが出てくるたびにこいつを入れていけばいいねappbird.icon
上手く書けば関数型言語っぽくきれいにまとまるもんだね
code:rs
let points =
reader.read_positions().expect("The model data doesn't have position data.")
.map(|point| Vec4::newpoint(point0 as f64, point1 as f64, point2 as f64)) .map(|p| Vec4Model(p))
.collect::<Vec<_>>();
let colors =
reader.read_colors(0).expect("The model data doesn't have color data.")
.into_rgb_f32()
.map(|color| Vec4::newvec(color0 as f64, color1 as f64, color2 as f64)) .collect::<Vec<_>>();
assert_eq!(points.len(), colors.len());
let attribute =
points.into_iter()
.zip(colors)
.map(|(point, color)| color_pipeline::Attribute{ point, color })
.collect::<Vec<_>>();
let idx =
reader.read_indices()
.expect("The model data doesn't have color data.")
.into_u32()
.map(|u| u as usize)
.collect::<Vec<_>>()
.chunks_exact(3)
.map(|idx| [idx0, idx1, idx2]) .collect::<Vec<_>>();
vao_seq.push(VAOColor { attribute, idx });
というわけで、VAOだけ受け取ってあとは何もしない新しいActorとしてGltfTestActorを作って...
mainのコードをこうする!
code:rs
fn main() -> Throwable<()> {
let w: usize = 640;
let h: usize = 480;
let aspect = (h as f64) / (w as f64);
let mut canvas = Canvas::new(w, h)?;
let mut camera = Camera::new(aspect);
let mut world = World::<GltfTestActor>::new();
let vao_seq = load_vao(&Path::new("./resource/test_model.glb"))?;
vao_seq.into_iter()
.map(|vao| GltfTestActor::new(vao))
.for_each(|actor| world.spawn(actor));
while canvas.update()? {
let t = canvas.passed_time();
let theta = 2.*PI/5. * t;
let r = 3.;
camera.position = Vec4::newpoint(r* f64::cos(theta), r*f64::sin(theta), 1.);
camera.look = -camera.position.normalized3d();
camera.up = Vec4::newvec(0., 0., 1.);
world.update(canvas.deltatime());
world.draw(&camera, &mut canvas);
}
Ok(())
}
https://gyazo.com/404ebfc40e264c32c3a10bb11f8fee13
おおっ...!ちゃんと読み取れてる!!
ただ問題がいくつか...appbird.icon
1. なんか妙に穴が空いているように見える。
ポリゴンとポリゴンの合間が見える....appbird.icon
ポリゴンとポリゴンの合間も塗るようにしたい
一回確認してみるか
2. cullingをonにすると正しく描画されなくなる
モデル側の問題か?
---> そうだったとして、それをどう確かめる?appbird.icon
全部のポリゴンに対して面積を表示させる...?
一番目、二番目、三番目のそれぞれのポリゴンに着色するのもありかも。
というか、tetrahedronのデモでも表示が変になるならそれはプログラムが変ってことだ
それともrendererの問題か?
https://gyazo.com/1f5ab652a2125ab8f3674d3b401d6d9c
2025-10-16
1. なんか妙に穴が空いているように見える。
不等号の取り扱いに関する問題かと思ったがどうやら違うらしい
code:component/mesh_renderer.rs
let u_intv = ClosedInterval::between(0., 1.);
if !w.iter().all(|e| u_intv.includes(*e)) { continue; }
code:util/interval.rs
pub fn includes(&self, x:T) -> bool {
self.min <= x && x <= self.max
}
浮動小数点に関する誤差の話かと思い1e-6だけ広げてみたけどこれまたうーんって感じ。
これなんで三角対角線の部分はあんまり穴が空いてないんだ?
グリッドの四角辺の部分だけ穴が空いている...
アイデアがない...。
本来頂点としては共有してるはずなので、穴が空くハズはないんだけどな~~
https://gyazo.com/9e147e1b48df2ab66bca8009ea52c3e9
...穴がxy軸に平行に空いている?
これ、そもそも三角形を包むbounding boxを求める際の、x_segmentとかy_segmentとかの話ではなかろうか
いやでもrangeとかandとかor演算とかに誤りはないな...
code:mesh_renderer.rs
fn calc_bounding_box(canvas: &mut Canvas, points: &&Vec4; 3) -> (ClosedInterval, ClosedInterval, ClosedInterval<f64>) { let bound_x = ClosedInterval::between(0,(canvas.width-1) as i32);
let bound_y = ClosedInterval::between(0,(canvas.height-1) as i32);
let bound_z = ClosedInterval::between(-1.,1.);
let x_segment = ClosedInterval::range(points.iter().map(|p| p.x() as i32));
let y_segment = ClosedInterval::range(points.iter().map(|p| p.y() as i32));
let z_segment = ClosedInterval::range(points.iter().map(|p| p.z()));
let x_segment = x_segment.and(&bound_x);
let y_segment = y_segment.and(&bound_y);
let z_segment = z_segment.and(&bound_z);
(x_segment, y_segment, z_segment)
}
待てよ?これIteratorとして実装していたよね?
もしかして、ClosedIntervalにもかかわらずendが含まれてないように実装したんじゃないか?
code:rs
impl Iterator for ClosedIntervalIter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current;
self.current = current + 1;
if self.end > current {
Some(current)
} else {
None
}
}
}
...あのさぁ....
code:rs
if self.end > current {
code:rs
if self.end >= current {
はい。
よっしゃ動いたぞ動いた動いた
滑らかですね
https://gyazo.com/958fdc728cb925fcceb6101294afb79c
2. cullingをonにすると正しく描画されなくなる
code:mesh_renderer.rs
// y基準でソート
let (x_segment, y_segment, z_segment) = calc_bounding_box(canvas, &points);
if z_segment.is_empty() { return; }
// Barycentric座標
let area_abc = area(&points0, &points1, &points2); // culling
if self.culling && area_abc < 0. { return; }
cullingが関わってくるのはこの部分だけ...。
符号付和が上手く計算できていなさそうだなぁ
ほとんどの場合においてみんな裏側になっているらしい、なんでだろうね
というわけで、少しばかし道具立てしてきて、符号付和の計算が毎フレーム何回行われるかを集積するようにしてみた。
シングルトンを用いて、いつでもどこでも簡単に呼び出せるやつを作っておいた。 Rustでのシングルトンなので、マルチスレッドから触られると困るので、毎回lock, unwrapする必要はある。
まぁでもデバッグ用なのでとりあえずはヨシかな。
これでどれぐらいのポリゴンが毎フレーム表を向いていると判定されているかを測れる。
code:mesh_renderer.rs
// culling
TRIANGLE_COUNTER.lock().unwrap().count(area_abc >= 0.);
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
code:.rs
while canvas.update()? {
:
{
let mut logger_on_loop = LOGGER_ON_LOOP.lock().unwrap();
let mut triangle_counter = TRIANGLE_COUNTER.lock().unwrap();
logger_on_loop.println(&triangle_counter.describe())?;
logger_on_loop.flush()?;
triangle_counter.reset();
}
}
するとこうなった。
culling off
code:rs
TRIANGLE_COUNTER: 600 / 1152
おおよそ600あたりを前後する。
ふむ、どうやら表裏の判定はできているようだ
culling on
code:rs
TRIANGLE_COUNTER: 1 / 1
TRIANGLE_COUNTER: 3 / 3
TRIANGLE_COUNTER: 5 / 5
:
...だいぶ奇々怪々な動きしてますね...
つまり本来描画されるべきだったポリゴンすらこの前のコードの段階で削られていることがわかる
z_segmentがemptyになっていることが原因かと見たが、どうにもそうではないらしい。
varyingやpolygonsが問題ではないっぽいことはわかった
どちらも元の数通り1152個のポリゴンを列挙すること自体はできていた。。
...じゃあどこで実行回数が減ってるんだ!?
code:rs
TRIANGLE_COUNTER.lock().unwrap().count( area_abc >= 0.);
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
ん...???
code:rs
if self.culling && area_abc < 0. { **return;** }
if area_abc.abs() < 1e-6 { **return;** }
んんんんんんん????????
return;で帰っちゃループが終わっちゃうじゃんかよ!!!
cullingできた... でき.......あれ...?
https://gyazo.com/7ac3f65e5dd9c984e9408317c5d60c32
なんか....逆じゃないか?
裏側が見えてるような気がするんだが...appbird.icon
code:rs
if self.culling && area_abc > 0. { continue; }
https://gyazo.com/d29bab014c7244f1d1515d5cfadaba56
うーーーん....?areaの計算が間違えている?
頂点が反時計回りだと正なんだっけ?
はいGPT.icon
じゃああってるな....
....glTFの座標系は右手?左手?appbird.icon
📦 glTF 仕様の座標系GPT.icon
右手系 (Right-handed)
上方向 (Up): +Y
前方向 (Forward): -Z
...もう一つ質問いいかな。Blenderでの座標系は?appbird.icon
🧭 Blender の内部座標系GPT.icon
右手系 (Right-handed)
上方向 (Up): +Z
前方向 (Forward): -Y
→ つまり、Blender内で「前を向くオブジェクト」は -Y 方向を向いています。
自作レンダラーなどで glTF の頂点座標をそのまま読み込む場合、glTF の座標系(+Y up, -Z forward)に合わせて処理しないと、モデルが「寝ている」「裏返っている」ように見えます。GPT.icon
あぁぁぁぁ~~~~~~appbird.icon
それだ。。。 なんかモデルの向きがBlenderで見たものとは違うな、とは思ったが......appbird.icon
添え字を入れ替えるか
code:mesh/mesh_loader.rs
let points =
reader.read_positions().expect("The model data doesn't have position data.")
.map(|point| Vec4::newpoint(point0 as f64, point2 as f64, point1 as f64)) .map(|p| Vec4Model(p))
.collect::<Vec<_>>();
code:rs
if self.culling && area_abc < 0. { continue; }
https://gyazo.com/1eade565fe9711c8a798502f244ea151
き......きたぁぁぁぁあぁぁぁぁあぁぁぁああああ!!!!!!!!!!!!!!!!!
PLYファイルをOBJ形式に変換したい....できるのか?
For converting PLY to OBJ/3DS formats, there used to be a free demo version of Deep Exploration, available here, but we hear it is no longer available.
ほう、一応アクセスしてみるか
...オンラインカジノの紹介サイトになっとるやないかい!!!!
フロリダのカジノがどうたらとか言うとる、怖いねえ
Bruce Merry of South Africa has written a script to import PLY files to Blender. Click here to download it.
うーんこれもリンク切れか....
公式に用意された手段では読み込むことは難しそう、かな?
あ、今のBlenderはplyファイルを読めるっぽいな?
http通信経由でDLしないといけない関係上、いったんブロックされてしまうが...
あぁ、本当に点群のデータだこれ...
思ったよりダウンロードが難しいな...?
こういうのもあるでGPT.icon
なるほどappbird.icon